/*    Copyright 2012 Tobias Marschall
 *
 *    This file is part of MoSDi.
 *
 *    MoSDi is free software: you can redistribute it and/or modify
 *    it under the terms of the GNU General Public License as published by
 *    the Free Software Foundation, either version 3 of the License, or
 *    (at your option) any later version.
 *
 *    MoSDi is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU General Public License for more details.
 *
 *    You should have received a copy of the GNU General Public License
 *    along with MoSDi.  If not, see <http://www.gnu.org/licenses/>.
 */

package mosdi.paa.apps;

import mosdi.paa.DAA;
import mosdi.util.Alphabet;
import mosdi.util.AminoAcidMasses;

/** DAA that determines whether a fragment within a given mass range
 *  occurs in a (cleaved) protein. Assumes trypsin cleavage rules. */
public class MassOccurrenceDAA extends DAA {
	private double accuracy;
	private int minMass;
	private int maxMass;
	Alphabet alphabet;
	
	/** Constructor.
	 * @param massAccuracy Mass unit w.r.t. which masses are discretized; e.g., a value
	 *                     of 0.1 means that masses are handled in discrete units of 0.1Da.
	 * @param minMass Lower end point of the mass range of interest.
	 * @param maxMass Upper end point of the mass range of interest.
	 */
	public MassOccurrenceDAA(double massAccuracy, double minMass, double maxMass) {
		this.accuracy = massAccuracy;
		this.minMass = (int)Math.round(minMass / accuracy);
		this.maxMass = (int)Math.round(maxMass / accuracy);
		if (this.minMass > this.maxMass) throw new IllegalArgumentException();
		alphabet = Alphabet.getAminoAcidAlphabet();
	}
	
	@Override
	public int getAlphabetSize() {
		return alphabet.size();
	}

	@Override
	public int getStartState() {
		return 0;
	}

	@Override
	public int getStateCount() {
		// state 0: start state
		// state 1 to alphabet.size(): regular states encoding the last read character
		// state alphabet.size()+1 to 2*alphabet.size(): like regular state, but there was a 
		//                                               breakpoint between last and second last character
		return 2 * alphabet.size() + 1;
	}

	private boolean isCleavageCharacter(int c) {
		return (c == alphabet.getIndex('K')) || (c == alphabet.getIndex('R'));
	}

	private boolean isProhibitionCharacter(int c) {
		return c == alphabet.getIndex('P');
	}

	@Override
	public int getTransitionTarget(int state, int character) {
		if (state == 0) return character + 1;
		state = (state - 1) % alphabet.size();
		int result = 1 + character;
		boolean cleavage = isCleavageCharacter(state) && !isProhibitionCharacter(character);
		if (cleavage) result += alphabet.size();
		return result;
	}

	/** Returns the index of the amino acid associated with the given state.
	 *  If there is none (e.g. for the start state), -1 is returned. */
	public int getAminoAcid(int state) {
		if ((state < 0) || (state > 2*alphabet.size())) throw new IllegalArgumentException();
		if (state == 0) return -1;
		return (state-1) % alphabet.size();
	}
	
	/** Returns an array of values that indicate that the sought mass has been found
	 *  in any fragment (including the last). */
	public int[] massFoundValues() {
		int[] result = new int[maxMass-minMass+2];
		int i = 0;
		for (int v=minMass; v<=maxMass; ++v,++i) {
			result[i] = v;
		}
		result[result.length - 1] = maxMass + 2;
		return result;
	}

	/** Returns true if the given value indicates that the sought mass has been found
	 *  in any fragment (including the last). */
	public boolean massFound(int value) {
		if ((value<0) || (value>maxMass+2)) throw new IllegalArgumentException();
		if (value == maxMass + 2) return true;
		return (value >= minMass) && (value <= maxMass);
	}
	
	@Override
	public int getEmission(int state) {
		int aa = getAminoAcid(state);
		if (aa < 0) return 0;
		return AminoAcidMasses.getMainMass(aa, accuracy);
	}

	@Override
	public int getEmissionCount() {
		return 1 + alphabet.size();
	}

	@Override
	public int getStartValue() {
		return 0;
	}

	@Override
	public int getValueCount() {
		return maxMass + 3;
	}

	@Override
	public int performOperation(int state, int value, int emission) {
		// maxMass+1 means "mass > maxMass"
		// maxMass+2 represents the absorbing state entered when the sought mass
		// has been observed.
		if (value == maxMass+2) return value;
		boolean cleavage = state > alphabet.size();
		if (cleavage) {
			if ((value >= minMass) && (value <= maxMass)) {
				return maxMass + 2;
			} else {
				return Math.min(emission, maxMass + 1);
			}
		} else {
			return Math.min(value + emission, maxMass + 1);
		}
	}

}
